iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0
Modern Web

從 Next.js 開始的 Functional Programming系列 第 21

D21 - 實作異步流程 (七)

  • 分享至 

  • xImage
  •  

異步流程扯東扯西不知不覺竟然已經講了七天了!
實在是不好意思再拖更久,預計接下來三天一定要把後端的步驟 3 / 4 / 5 講完 (ง๑ •̀_•́)ง (ง๑ •̀_•́)ง (ง๑ •̀_•́)ง

程式碼請參考 D20/asynchronous-refactor

安裝 MSW

  1. 安裝時加上 -D,讓它不會被包裝進 production 裡面

    npm i -D msw
    
  2. 以 Next.js 專案來說,直接在專案跟目錄執行以下指令,它就會把它依賴的 JS 檔案放到 public 路徑供你在執行階段取用

    npx msw init public/ --save
    

Server 與 Worker

https://ithelp.ithome.com.tw/upload/images/20231006/20158615ataYvPyOxx.png

前面有說到 MSW 可以同時適用於瀏覽器環境與伺服器環境。其中執行於瀏覽器的部分叫做 worker 、執行於伺服器環境的叫 server

server 這個名字好理解, worker就比較特別了。其實 worker 背後跟 chorme 引擎的功能 service worker 有關,你可以想像 service worker 是一個運行於瀏覽器的 UI 執行緒之外的獨立執行緒,藉由 service worker 的幫助,我們就可以攔截前端程式發出的請求,並且把假的回應丟回去給 UI 執行緒。

MSW Handlers

rest.get(/* url */, (req,res,ctx)=>res(ctx.json({/* body */})))

msw 的 handler 語法很簡單

  • rest 代表 restful API,另外有支援 graphql 我們先不討論
  • req 是請求物件,我們可以從中拿到 body / query / params
  • res 代表送出回應
  • ctx 決定回應內容

為了方便使用,我們可以在建立 msw handler 時利用一些小技巧依照 endpoint 做整理,例如這樣

import { rest } from 'msw'
import { Resolver } from './utils'

const resolvers = {
  '200 admin': (): Resolver => (req, res, ctx) =>
    res(
      ctx.status(200),
      ctx.json({ _tag: 'Administrator', name: req.params.username })
    ),
  '200 admin once': (): Resolver => (req, res, ctx) =>
    res.once(
      ctx.status(200),
      ctx.json({ _tag: 'Administrator', name: req.params.username })
    ),
  '200 invalid': (): Resolver => (req, res, ctx) =>
    res(ctx.status(200), ctx.json('lol')),
  '404': (): Resolver => (req, res, ctx) => res(ctx.status(404)),
  '404 once': (): Resolver => (req, res, ctx) => res.once(ctx.status(404)),
  '500': (): Resolver => (req, res, ctx) => res(ctx.status(500)),
}

export const getUsersByNameHandler =
  (baseUrl: string) => (key: keyof typeof resolvers) =>
    rest.get(`${baseUrl}/api/v1/users/:username`, resolvers[key]())

使用時就會很方便、好讀
https://ithelp.ithome.com.tw/upload/images/20231006/20158615imcG7mAXOp.png

MSW + Vitest @ Node

接下來介紹如何在 node.js 環境使用 MSW,以下範例不只是 vitest,像是 jest, mocha 等測試套件也都可以參考以下方法使用。

describe('UsersField.on AddUserEvent', () => {
  const baseUrl = 'http://localhost'
  const server = initServer(baseUrl)
  beforeAll(() => server.listen())
  afterEach(() => server.resetHandlers())
  afterAll(() => server.close())

  it('should ....'){
	  // all requests handled by msw
  }
}
  • base url 在 node 環境一定要寫完整,通常都會是 http://localhost
  • 在全部測試開始之前啟用 server
  • 在每個測試結束之後重置 handler
  • 在全部測試結束之後關閉 server

MSW + Cypress @ Browser

MSW 可以搭配 vitest 做 unitest 當然也就可以搭配 cypress 做 component test。

describe('<Users />', () => {
  const worker = initWorker('')

  before(() => {
    worker.start()
  })

  beforeEach(() => {
    cy.mount(<Users />)
  })

  afterEach(() => {
    worker.resetHandlers()
  })

  after(() => {
    worker.stop()
  })
  • base url 在瀏覽器環境,如果請求都是打自己的 host,可以留空
  • 在全部測試開始之前啟動 server,注意這邊和 server 不同,一個是 listen 一個是 start
  • 在每個測試結束之後重置 handler
  • 在全部測試結束之後關閉 server,這邊也和 server 不同,一個是 close 一個是 stop

MSW use

不論是 server 或是 worker,都有看到一個 resetHandler 的動作,這是甚麼意思呢? 難道說這些 handler 還可以中途變動? 沒錯,真的可以測到一半改 Handler ! 也正是因為怕影響到後續測試,所以我們才每次都做 resetHander !

  it('should show 1 error message when user not found on 1 user selected', () => {
    worker.use(getUsersByNameHandler('')('404'))
    worker.use(getUsersByNameHandler('')('200 admin once'))
    cy.get('input').type('richard_w{Enter}')
    cy.get('.user-badge')
    cy.get('input').type('richard{Enter}')
    cy.get('.error-message').should('have.length', 1)
  })

用以上範例來說明,我們第一個 worker.use 把原本都會回 200 替換成 404 的 mock API 替換成了都回 404,然後再補上一個會回一次 200 然後就失效的 handler,這樣一來我們第一個請求就會回 200,然後後面都回 404

  it('should have not found error when status 404', async () => {
    //arrange
    server.use(getUsersByNameHandler(baseUrl)('404'))
    const event = AddUserEvent.self
    //act
    const result = await pipe(
      UsersField.on(event)(state),
      Effect.merge,
      Effect.runPromise
    )
    //assert
    expect(result._tag).toBe('InvalidUsersField')
    if (Option.isSome(result.error)) {
      expect(result.error.value._tag).toBe('NotFoundError')
    } else {
      expect(result.error._tag).toBe('Some')
    }
  })

把場景轉換到 vitest 也是一樣,只是名字從 worker.use 換成 server.use,就這麼簡單 !


上一篇
D20 - 實作異步流程 (六)
下一篇
D22 - 實作異步流程 (八)
系列文
從 Next.js 開始的 Functional Programming30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言